﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.IO;
using System.Xml;

namespace asoptions
{
    class MusicLibrary
    {
        private List<TreeNode> roots;
        public List<TreeNode> getRoots()
        {
            return roots;
        }
        public List<string> musicDirs;

        public string xmlPath { get; set; }
        private MainWindow window;
        private XmlWriter writer;
        private int totalFiles;
        private int procFiles;

        public MusicLibrary(MainWindow win)
        {
            window = win;
            xmlPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
            xmlPath = System.IO.Path.Combine(xmlPath, ".asoptions\\lib.xml");
            roots = new List<TreeNode>();
            musicDirs = new List<string>();
        }

        /// <summary>
        /// Checks if the given list of directories matches the current music library.
        /// Note: Even if the library is not up to date, this can return true. For example,
        /// when the program launches musicDirs.Count == 0 && libFoldersListBox.Items.Count == 0.
        /// If this is the case, then we probably need to read the saved library from xml.
        /// </summary>
        /// <param name="dirs">List of directories. Meant to come from libFoldersListBox.Items.</param>
        /// <returns></returns>
        private bool containsDir(ListBox.ObjectCollection dirs)
        {
            foreach (var dir in dirs)
            {
                if (!musicDirs.Contains((string)dir))
                    return false;
            }
            return true;
        }

        /// <summary>
        /// Update the music library. Results are stored in roots (use getRoots() after calling
        /// this method)
        /// </summary>
        /// <param name="dirs">List of directories to add. Meant to come from libFoldersListBox.Items.</param>
        public void createLibrary(ListBox.ObjectCollection dirs)
        {
            if (File.Exists(xmlPath) && containsDir(dirs))
            {
                readXML();
                window.libFoldersListBox_setItems(musicDirs);
            }
            else
            {
                musicDirs.Clear();
                foreach (var path in dirs)
                    musicDirs.Add((string)path);
                refreshLibrary();
            }
        }

        /// <summary>
        /// Refresh the library. This scans all folders in the library and creates nodes, storing
        /// results in roots. Results are also saved at xmlPath.
        /// </summary>
        private void refreshLibrary()
        {
            window.addToLog("[Refreshing library]", MessageType.Info);
            roots.Clear();
            if (musicDirs.Count == 0)
            {
                window.addToLog("No music directories in library. Please add a music folder in the \"Other Options\" tab.", MessageType.Error);
                roots.Clear();
                window.libraryRefreshButtonEnabled(true);
                return;
            }
            totalFiles = 0; procFiles = 0;
            foreach (var path in musicDirs)
            {
                DirectoryInfo dir = new DirectoryInfo(path);
                totalFiles += window.exts.AsParallel().SelectMany(searchPattern=>dir.EnumerateFiles(searchPattern, SearchOption.AllDirectories)).Count();
            }
            foreach (var path in musicDirs)
            {
                DirectoryInfo dir = new DirectoryInfo(path);
                roots.Add(createDirectoryNode(dir));
            }
            File.Delete(xmlPath);
            System.Threading.Thread wrxml = new System.Threading.Thread(new System.Threading.ThreadStart(writeXML));
            wrxml.Start();
        }

        /// <summary>
        /// Read from the file specified by xmlPath. Does not currently check if the file exists or
        /// if it is formatted correctly.
        /// </summary>
        private void readXML()
        {
            musicDirs.Clear();
            roots.Clear();
            window.addToLog("[Loading saved library]", MessageType.Info);
            XmlDocument doc = new XmlDocument();
            try
            {
                doc.Load(xmlPath);
            }
            catch (XmlException ex)
            {
                window.addToLog("Library xml parse error. Refreshing library.", MessageType.Error);
                refreshLibrary();
                return;
            }
            for (int k = 0; k < doc.DocumentElement.ChildNodes.Count; k++)
            {
                TreeNode root = new TreeNode("root");
                readSubNode(doc.DocumentElement.ChildNodes[k], root);
                roots.Add(root);
            }
            window.addToLog("[Library load complete]", MessageType.Info);
            window.libraryRefreshButtonEnabled(true);
        }

        /// <summary>
        /// Recursively process subnodes of an XmlNode. Creates a tree starting at the given TreeNode
        /// </summary>
        /// <param name="node">Root XmlNode</param>
        /// <param name="parent">Root TreeNode</param>
        private void readSubNode(XmlNode node, TreeNode parent)
        {
            if (node.HasChildNodes)
            {
                if (node.Name == "song")
                {
                    parent.Tag = NodeType.Song;
                    parent.ToolTipText = "Artist: " + node.ChildNodes[0].InnerText + "\n" +
                                         "Album: " + node.ChildNodes[1].InnerText + "\n" +
                                         "Title: " + node.ChildNodes[2].InnerText;
                }
                else if (node.Name == "optag")
                    parent.Tag = NodeType.OptionTag;
                else
                {
                    if (node.Name == "root")
                    {
                        XmlAttributeCollection xac = node.Attributes;
                        musicDirs.Add(xac["path"].Value);
                        parent.Text = xac["label"].Value;
                        parent.Name = xac["path"].Value;
                        parent.Tag = NodeType.Folder;
                    }
                    for (int k = 0; k < node.ChildNodes.Count; k++)
                    {
                        XmlAttributeCollection xac = node.ChildNodes[k].Attributes;
                        TreeNode tn = new TreeNode(xac["label"].Value);
                        tn.Name = xac["path"].Value;
                        switch (node.ChildNodes[k].Name)
                        {
                            case "folder":
                                tn.Tag = NodeType.Folder;
                                break;
                            case "song":
                                tn.Tag = NodeType.Song;
                                break;
                            case "options":
                                tn.Tag = NodeType.Tag;
                                break;
                            case "optag":
                                tn.Tag = NodeType.OptionTag;
                                break;
                        }
                        parent.Nodes.Add(tn);
                        readSubNode(node.ChildNodes[k], tn);
                    }
                }
            }
        }

        /// <summary>
        /// Write the music library info to an xml file
        /// </summary>
        private void writeXML()
        {
            window.addToLog("[Saving library]", MessageType.Info);
            XmlWriterSettings settings = new XmlWriterSettings();
            settings.Indent = true;
            settings.IndentChars = "    ";
            writer = XmlWriter.Create(xmlPath, settings);
            writer.WriteStartElement("library");
            foreach (var node in roots)
            {
                writer.WriteStartElement("root");
                writer.WriteAttributeString("label", node.Text);
                writer.WriteAttributeString("path", node.Name);
                writeSubNode(node);
                writer.WriteEndElement();
            }
            writer.WriteEndElement();
            writer.Flush();
            writer.Close();
            window.addToLog("[Library saved]", MessageType.Info);
            window.libraryRefreshButtonEnabled(true);
        }

        /// <summary>
        /// Recursively create the xml for all subnodes
        /// </summary>
        /// <param name="parent">TreeNode whose subnodes will be scanned</param>
        private void writeSubNode(TreeNode parent)
        {
            for (int k = 0; k < parent.Nodes.Count; k++)
            {
                // <folder label="folder name" path="full\path\including\folder name">
                if ((NodeType)parent.Nodes[k].Tag == NodeType.Folder)
                {
                    writer.WriteStartElement("folder");
                    writer.WriteAttributeString("label", parent.Nodes[k].Text);
                    writer.WriteAttributeString("path", parent.Nodes[k].Name);
                    writeSubNode(parent.Nodes[k]);
                    writer.WriteEndElement();
                }
                // <song label="filename.ext" path="full\path\including\filename.ext">
                //   <artist>Artist</artist>
                //   <album>Album</album>
                //   <title>Title</title>
                else if ((NodeType)parent.Nodes[k].Tag == NodeType.Song)
                {
                    writer.WriteStartElement("song");
                    writer.WriteAttributeString("label", parent.Nodes[k].Text);
                    writer.WriteAttributeString("path", parent.Nodes[k].Name);
                    TagLib.File f = TagLib.File.Create(parent.Nodes[k].Name);
                    writer.WriteElementString("artist", f.Tag.FirstPerformer);
                    writer.WriteElementString("album", f.Tag.Album);
                    writer.WriteElementString("title", f.Tag.Title);
                    if (parent.Nodes[k].Nodes.Count > 0)
                    {
                        writeSubNode(parent.Nodes[k]);
                    }
                    writer.WriteEndElement();
                }
                // <options label="Options">
                // NOTE: should only be within <song> elements
                else if ((NodeType)parent.Nodes[k].Tag == NodeType.Tag)
                {
                    writer.WriteStartElement("options");
                    writer.WriteAttributeString("label", "Options");
                    writeSubNode(parent.Nodes[k]);
                    writer.WriteEndElement();
                }
                // <optag>tag</optag>
                // NOTE: should only be within <options> elements
                else if ((NodeType)parent.Nodes[k].Tag == NodeType.OptionTag)
                {
                    writer.WriteElementString("optag", parent.Nodes[k].Text);
                }
            }
        }

        /// <summary>
        /// Create a TreeNode for a directory that contains all its subdirectories
        /// </summary>
        /// <param name="dir">Root directory</param>
        /// <returns>TreeNode for directory dir</returns>
        private TreeNode createDirectoryNode(DirectoryInfo dir)
        {
            TreeNode dirnode = new TreeNode(dir.Name);
            dirnode.Name = dir.FullName;
            dirnode.Tag = NodeType.Folder;

            // add music nodes to the current directory node
            foreach (var file in window.exts.AsParallel().SelectMany(searchPattern => dir.EnumerateFiles(searchPattern)))
            {
                TreeNode node = MusicLibrary.createMusicNode(file);
                node.Collapse();
                dirnode.Nodes.Add(node);
                procFiles++;
                window.backgroundWorker1.ReportProgress((int)(((float)procFiles / (float)totalFiles) * 100));
                //window.libraryToolStripProgressBar.PerformStep();
            }
            // add subdirectories recusively
            foreach (var subdir in dir.EnumerateDirectories())
            {
                TreeNode node = createDirectoryNode(subdir);
                dirnode.Nodes.Add(node);
            }
            return dirnode;
        }

        /// <summary>
        /// Creates a node for the given music file. Sub-nodes include information from tags
        /// </summary>
        /// <param name="file">The file to scan for info</param>
        /// <returns>TreeNode with name=[file name]</returns>
        public static TreeNode createMusicNode(FileInfo file)
        {
            TagLib.File f = TagLib.File.Create(file.FullName);
            TreeNode node = new TreeNode(file.Name);
            node.Name = file.FullName;
            node.Tag = NodeType.Song;
            node.ToolTipText = "Artist: " + f.Tag.FirstPerformer + "\n" +
                               "Album: " + f.Tag.Album + "\n" +
                               "Title: " + f.Tag.Title;
            var ops = Tagger.getOptions(f);
            if (ops != null)
            {
                node.Nodes.Add("Options", "Options");
                foreach (var op in ops)
                {
                    TreeNode opnode = new TreeNode(op);
                    opnode.Tag = NodeType.OptionTag;
                    node.Nodes[0].Nodes.Add(opnode);
                }
            }
            for (int k = 0; k < node.Nodes.Count; k++)
                node.Nodes[k].Tag = NodeType.Tag;
            node.ExpandAll();
            return node;
        }
    }
}
